﻿using RockBus.Configuration.Interfaces;
using RockBus.DataContracts;
using RockBus.Logging.Interfaces;
using RockBus.Messages.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Channels;
using System.Threading.Tasks;
using System.Transactions;
using System.Xml;
using RockBus.ServiceModel;

namespace RockBus.Messages
{
    public class MessageFactory : IMessageFactory
    {
        public MessageFactory(ILogger logger, IInternalConfiguration configuration)
        {
            this.Logger = logger;
            this.Configuration = configuration;
        }

        public Message CreateMessage(XmlReader xmlReader, string action, Message message)
        {
            var newMessage = Message.CreateMessage(message.Version, action, xmlReader);
            newMessage.Properties.CopyProperties(message.Properties);
            return newMessage;
        }

        public ILogger Logger { get; private set; }

        public IInternalConfiguration Configuration { get; private set; }

        /// <summary>
        /// Creates a new IMessage from a System.ServiceModel.Channels.Message
        /// </summary>
        /// <param name="message"></param>
        /// <param name="useBuffer"></param>
        /// <returns></returns>
        public IMessage CreateMessage(Message message, bool useBuffer)
        {
            return new BufferedMessage(message, useBuffer);
        }

        /// <summary>
        /// Creates a= new IMessage from an XmlReader with the specified SOAP Action
        ///  Copies the MessageVersion and MessageProperties from the specified IMessage
        /// </summary>
        /// <param name="xmlReader"></param>
        /// <param name="action"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public IMessage CreateMessage(XmlReader xmlReader, string action, IMessage message)
        {
            var newMessage = Message.CreateMessage(message.Version, action, xmlReader);
            newMessage.Properties.CopyProperties(message.Properties);
            return this.CreateMessage(newMessage, true);
        }

        public IMessageContext CreateMessageContext(XmlReader reader, MessageVersion version)
        {
            Message message = Message.CreateMessage(reader, 4096, version);

            int messageHeaderIndex = message.Headers.FindHeader(RoutingHeader.HeaderName, StringConstants.RockBusNamespace);
            if (messageHeaderIndex <= 0)
            {
                throw new InvalidOperationException("Cannot construct MessageContext without RoutingHeader.");
            }

            RoutingHeader routingHeader = message.Headers.GetHeader<RoutingHeader>(messageHeaderIndex);
            message.Headers.RemoveAt(messageHeaderIndex);

            return this.CreateMessageContext(message, routingHeader);
        }

        public void ExtractHeaders(Message requestMessage, out RoutingHeader routingHeader, out CorrelationHeader correlationHeader)
        {
            routingHeader = null;
            int messageHeaderIndex = requestMessage.Headers.FindHeader(RoutingHeader.HeaderName, StringConstants.RockBusNamespace);
            if (messageHeaderIndex >= 0)
            {
                routingHeader = requestMessage.Headers.GetHeader<RoutingHeader>(messageHeaderIndex);
                requestMessage.Headers.RemoveAt(messageHeaderIndex);
            }

            correlationHeader = null;
            messageHeaderIndex = requestMessage.Headers.FindHeader(CorrelationHeader.HeaderName, StringConstants.RockBusNamespace);
            if (messageHeaderIndex >= 0)
            {
                correlationHeader = requestMessage.Headers.GetHeader<CorrelationHeader>(messageHeaderIndex);
                requestMessage.Headers.RemoveAt(messageHeaderIndex);
            }
        }

        public IMessageContext CreateRequestMessageContext(Message requestMessage, Transaction transaction, TaskCompletionSource<Message> tcs, ExchangeTypeEnum publisherExchangeType, ReceivePortInfo receivePortInfo, RoutingHeader routingHeader, CorrelationHeader correlationHeader)
        {
            if (routingHeader != null)
            {
                routingHeader.ProcessingData.MessageDirection++;
            }

            if (receivePortInfo != null)
            {
                publisherExchangeType = receivePortInfo.ReceiveEndpointInfo.ExchangeType;
            }

            var msg = this.CreateMessage(requestMessage, true);
            var mc = new MessageContext(this.Configuration, msg, transaction, routingHeader, correlationHeader, tcs, publisherExchangeType, receivePortInfo);
            return mc;
        }

        public IMessageContext CreateRequestMessageContextFromResponse(IMessageContext responseMessageContext)
        {
            var mc = new MessageContext(this.Configuration, responseMessageContext.ResponseMessage, null, null, null, null, ExchangeTypeEnum.OneWay, null, false);
            return mc;
        }

        public IMessageContext CreateResponseMessageContext(IMessageContext requestMessageContext, Message responseMessage)
        {
            switch (requestMessageContext.ProcessingData.MessageDirection)
            {
                case MessageDirectionEnum.InboundRequest:
                case MessageDirectionEnum.OutboundRequest:
                    requestMessageContext.ProcessingData.MessageDirection = MessageDirectionEnum.InboundResponse;
                    break;

                case MessageDirectionEnum.InboundResponse:
                case MessageDirectionEnum.OutboundResponse:
                    requestMessageContext.ProcessingData.MessageDirection = MessageDirectionEnum.End;
                    break;

                case MessageDirectionEnum.End:
                    throw new ArgumentException("Can't create a response from an ended message.");
                default:
                    break;
            }

            //responseMessage.Properties.SetMessageId(requestMessageContext.RequestMessage.Id);
            var response = this.CreateMessage(responseMessage, true);
            requestMessageContext.ResponseMessage = response;

            var rh = requestMessageContext.RoutingHeaders.First();
            rh.DestinationEndpoint = null;

            return requestMessageContext;
        }

        public IMessageContext CreateFaultMessageContext(IMessageContext requestMessageContext, Message responseMessage)
        {
            requestMessageContext.ResponseMessage = this.CreateMessage(responseMessage, true);
            requestMessageContext.Exception = new Exception("Message delivery failure due to fault exception.");
            return requestMessageContext;
        }

        public IMessageContext CreateMessageContext(Message message, RoutingHeader routingHeader)
        {
            var msg = this.CreateMessage(message, true);
            // MessageDirection filled from RoutingHeader
            return new MessageContext(this.Configuration, msg, routingHeader);
        }

        public IMessageContext CreatePersistentMessageContext(ProcessingData processingData, IMessage requestMessage, Transaction transaction, IEnumerable<RoutingHeader> persistentRoutingHeaders, TaskCompletionSource<Message> tcs, EvaluationResult evaluationResult, ReceivePortInfo receivePortInfo)
        {
            return new MessageContext(this.Configuration, processingData, requestMessage, transaction, persistentRoutingHeaders, tcs, evaluationResult, receivePortInfo);
        }

        public IMessageContext CreateTransientMessageContext(ProcessingData processingData, IMessage requestMessage, RoutingHeader routingHeader, TaskCompletionSource<Message> tcs, EvaluationResult evaluationResult, ReceivePortInfo receivePortInfo)
        {
            return new MessageContext(this.Configuration, processingData, requestMessage, routingHeader, tcs, evaluationResult, receivePortInfo);
        }

        /// <summary>
        /// Retrieves the System.ServiceModel.Channels.Message from the IMessageContext and adds the RoutingHeader, if so specified in the RoutingHeader's endpoint.
        /// </summary>
        /// <param name="messageContext"></param>
        /// <returns></returns>
        public Message CreateServiceModelMessage(IMessageContext messageContext, RoutingHeader routingHeader, MessageVersion messageVersion, bool addRoutingHeader, bool addCorrelationHeader)
        {
            var message = messageContext.Message.MarshalMessage(messageVersion);
            if (addRoutingHeader && (null != routingHeader))
            {
                var header = MessageHeader.CreateHeader(
                    RoutingHeader.HeaderName,
                    StringConstants.RockBusNamespace,
                    routingHeader);
                message.Headers.Add(header);
            }

            if (addCorrelationHeader)
            {
                var ch = new CorrelationHeader(messageContext.ProcessingData.CorrelationId, messageContext.ProcessingData.MessageId);
                var header = MessageHeader.CreateHeader(
                    CorrelationHeader.HeaderName,
                    StringConstants.RockBusNamespace,
                    ch);
                message.Headers.Add(header);
            }

#if DEBUG
            // In order to prevent duplicate WCF debugging headers, we remove it if it already exists here
            //  WCF will add a new debugging header for free when we send the message in debug mode
            int debuggingHeaderIndex = message.Headers.FindHeader("VsDebuggerCausalityData", "http://schemas.microsoft.com/vstudio/diagnostics/servicemodelsink");
            if (debuggingHeaderIndex > 0)
            {
                message.Headers.RemoveAt(debuggingHeaderIndex);
            }
#endif

            return message;
        }

        public Message CreatePersistenceMessage(IMessageContext messageContext, RoutingHeader routingHeader, MessageVersion transportMessageVersion)
        {
            var rh = routingHeader;
            var mv = messageContext.Message.GetMessageVersionAsString();
            var ma = messageContext.Message.Action;

            var rpiObj = messageContext.Message.Properties.GetPublisherReceivePortInfo();
            var rpi = (ReceivePortInfo)null;
            if (rpiObj != null)
            {
                rpi = rpiObj as ReceivePortInfo;
            }

            string ra = (string)messageContext.Message.Properties.GetReceiveAddress();
            if (null == ra)
            {
                ra = string.Empty;
            }

            var pl = messageContext.Message.ConvertToByteArray();

            var pm = new PersistenceMessage(rh, mv, ma, rpi, DateTime.UtcNow, ra, pl);
            return Message.CreateMessage(transportMessageVersion, StringConstants.PersistenceMessageAction, pm);
        }

        public IMessageContext CreateMessageContextFromPersistenceMessage(Message persistenceMessage, Transaction transaction, TaskCompletionSource<Message> tcs)
        {
            var pm = persistenceMessage.GetBody<PersistenceMessage>();
            var msg = pm.CreateMessage();

            return this.CreateRequestMessageContext(
                msg,
                transaction,
                tcs,
                pm.RoutingHeader.ProcessingData.PublisherExchangeType,
                pm.ReceivePortInfo,
                pm.RoutingHeader,
                pm.CorrelationHeader);
        }
    }
}